import { BusinessException } from '@libs/common/exceptions';
import { RedisService } from '@libs/common/redis';
import { indexPaginateBuilder } from '@libs/common/repository';
import { enumToObject, importJson, isDev } from '@libs/common/utils';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { ArrayToObject } from '@shencom/utils-array';
import { Dayjs, FormatDate, FormatDateTime } from '@shencom/utils-date';
import { plainToInstance } from 'class-transformer';
import { chunk } from 'lodash';
import { FindOptionsSelect, Repository, SelectQueryBuilder } from 'typeorm';

import { MemberService } from '@/dingding/member/member.service';

import type { ResAttendanceList } from '../../dingding/dingding.api';
import { DingdingService } from '../../dingding/dingding.service';
import { AccessLogService } from '../access-log/access-log.service';
import { ProviderKey } from './attendance.providers';
import {
  FindAttendanceDto,
  IndexAttendanceDto,
  StatisticsAttendanceDto,
  SyncAttendanceDto,
  TodayAttendanceDto,
} from './dto/attendance.dto';
import {
  Attendance,
  SourceType,
  TimeResult,
} from './entities/attendance.entity';

@Injectable()
export class AttendanceService {
  constructor() {}

  @Inject(RedisService)
  private redisService: RedisService;

  @Inject(DingdingService)
  private dingdingService: DingdingService;

  @Inject(MemberService)
  private memberService: MemberService;

  @Inject(AccessLogService)
  private accessLogService: AccessLogService;

  @Inject(ProviderKey)
  private repository: Repository<Attendance>;

  private async getAttendance(dto: { dateStart: string; dateEnd: string }) {
    const { dateStart, dateEnd } = dto;
    const workDateFrom = FormatDateTime(
      Dayjs(dateStart).startOf('day').valueOf(),
    );

    const workDateTo = FormatDateTime(Dayjs(dateEnd).endOf('day').valueOf());

    const memberList = await this.memberService.find(
      { status: 1 },
      {
        dingUserId: true,
        userId: true,
        deptNameList: true,
        name: true,
        position: true,
      },
    );

    const memberObj = ArrayToObject(memberList, 'dingUserId');

    const attendanceList = await this.dingdingService.AttendanceList({
      userIdList: memberList.map((v) => v.dingUserId),
      workDateFrom,
      workDateTo,
    });

    const results = attendanceList.map((v) => {
      const member = memberObj[v.userId]?.[0];

      if (!member) return plainToInstance(Attendance, v);

      return plainToInstance(Attendance, {
        ...v,
        name: member.name,
        deptNameList: member.deptNameList,
        position: member.position,
        userId: member.userId,
        dingUserId: member.dingUserId,
      });
    });

    return results;
  }

  private async batchUpdate(list: Attendance[]) {
    const Instance = plainToInstance(Attendance, list);
    await this.repository.save(Instance, { chunk: 100 });

    return Instance;
  }

  async index(dto: IndexAttendanceDto) {
    const res = await indexPaginateBuilder({
      dto,
      repository: this.repository,
    });
    return res;
  }

  private async findBase(
    dto: FindAttendanceDto,
    select?: FindOptionsSelect<Attendance>,
  ) {
    const raw = await this.repository.find({ where: dto, select });

    return raw;
  }

  /** 只返回一条对象数据，如果存在多个只返回第一条 */
  async findOne(...args: Parameters<AttendanceService['findBase']>) {
    const [dto, select] = args;
    const entity = await this.findBase(dto, select);

    if (Array.isArray(entity) && entity.length > 0) {
      return entity[0];
    }

    return null;
  }

  /** 返回所有符合条件的数据 */
  async find(...args: Parameters<AttendanceService['findBase']>) {
    const [dto, select] = args;
    return this.findBase(dto, select);
  }

  async statistics(dto: StatisticsAttendanceDto) {
    const queryBuilder = this.repository.createQueryBuilder('attendance');

    queryBuilder.where('attendance.dingId = :dingId', { dingId: dto.dingId });

    if (dto.start && dto.end) {
      if (!Dayjs(dto.start).isBefore(Dayjs(dto.end))) {
        throw new BadRequestException('开始时间大于结束时间');
      }

      queryBuilder.andWhere('attendance.workDate >= :start', {
        start: dto.start,
      });
      queryBuilder.andWhere('attendance.workDate <= :end', { end: dto.end });
    }

    queryBuilder
      .select('count(attendance.' + dto.type + ')', 'count')
      .groupBy('attendance.' + dto.type + '');

    if (dto.type === 'timeResult') {
      return this.statisticsTimeResult(queryBuilder);
    } else if (dto.type === 'sourceType') {
      return this.statisticsSourceType(queryBuilder);
    } else {
      throw new BadRequestException('type 类型错误');
    }
  }

  async statisticsTimeResult(queryBuilder: SelectQueryBuilder<Attendance>) {
    queryBuilder.addSelect('attendance.timeResult', 'type');

    const res = await queryBuilder.getRawMany<{
      count: string;
      type: string;
      label: string;
    }>();

    const timeResult = enumToObject(TimeResult);

    return res.map((v) => ({
      type: v.type,
      count: v.count,
      label: timeResult[v.type],
    }));
  }

  async statisticsSourceType(queryBuilder: SelectQueryBuilder<Attendance>) {
    queryBuilder.addSelect('attendance.sourceType', 'type');

    const res = await queryBuilder.getRawMany<{
      count: string;
      type: string;
      label: string;
    }>();

    const sourceType = enumToObject(SourceType);

    return res.map((v) => ({
      type: v.type,
      count: v.count,
      label: sourceType[v.type],
    }));
  }

  async sync(dto: SyncAttendanceDto) {
    const res = await this.getAttendance({
      dateStart: dto.workDateFrom,
      dateEnd: dto.workDateTo ?? FormatDateTime(),
    });

    return await this.batchUpdate(res);
  }

  /**
   * 定时获取钉钉前一天的考勤数据
   * 定时在每天中午 12 点执行前一天的考勤数据
   */
  @Cron('0 0 12 * * *')
  private async timingUpdateAttendance() {
    if (isDev) return;
    const workDate = FormatDate(Dayjs().subtract(1, 'day').valueOf());

    const res = await this.getAttendance({
      dateStart: workDate,
      dateEnd: workDate,
    });

    await this.batchUpdate(res);
  }

  /** 备份的数据写入 */
  async import(date?: string) {
    const data = await importJson<ResAttendanceList['recordresult'][number]>(
      '/data/attendance',
      date,
    );

    const lists = chunk(data, 1000);
    for (const list of lists) {
      const Instance = list.map((v) => {
        const item = plainToInstance(Attendance, { ...v, dingId: v.userId });
        return item;
      });
      await this.repository.save(Instance);
    }

    return;
  }

  async today(dto?: TodayAttendanceDto, user?: SC.User.Info) {
    if (user) {
      this.accessLogService.create({
        userName: user.realname || user.nickname,
        url: 'hr/attendance/today',
        userId: user.uid,
      });
    }

    try {
      const currentTime = Dayjs().valueOf();
      const workDate = FormatDate(dto?.workDate || currentTime);

      const key = `attendance_${workDate}`;

      // 从缓存中获取数据
      let attendanceList: Attendance[] = [];

      if (FormatDate(currentTime) === workDate) {
        attendanceList = (await this.redisService.get<Attendance[]>(key)) || [];
      }

      if (attendanceList?.length) return attendanceList;

      // 当天的数据不进行数据库查询
      if (FormatDate(currentTime) !== workDate) {
        // 从数据库中获取数据
        attendanceList = (await this.find({ workDate: dto?.workDate })) || [];
      }

      if (!attendanceList.length) {
        attendanceList = await this.getAttendance({
          dateStart: workDate,
          dateEnd: workDate,
        });
        this.batchUpdate(attendanceList);
      }

      if (!attendanceList.length) return [];

      const memberList = await this.memberService.find(
        { status: 1 },
        {
          dingUserId: true,
          deptNameList: true,
          name: true,
          position: true,
        },
      );

      const results: typeof attendanceList & typeof memberList = [];

      memberList.forEach((item) => {
        const member_attendance =
          attendanceList.filter((v) => v.dingUserId === item.dingUserId) || [];

        if (member_attendance.length) {
          member_attendance.forEach((v, i) => {
            results.push({ ...item, ...v, id: `${item.dingUserId}_${i}` });
          });
        } else {
          results.push({ ...item, id: item.dingUserId });
        }
      });

      let ttl = 60 * 10; // 默认缓存 10 分钟

      const rules = [
        { start: `${workDate} 09:10`, end: `${workDate} 12:30` },
        { start: `${workDate} 14:10`, end: `${workDate} 18:00` },
        {
          start: `${workDate} 20:00`,
          end: FormatDateTime(
            Dayjs(`${workDate} 08:50`).add(1, 'day').valueOf(),
          ),
        },
      ];

      for (const rule of rules) {
        const isBetween = Dayjs(currentTime).isBetween(rule.start, rule.end);

        if (isBetween) {
          // 存在范围内,那么缓存时间就是当前时间到 rule.end 时间是多少秒
          const end = Dayjs(rule.end).valueOf();

          ttl = (end - currentTime) / 1000;
          break;
        }
      }

      // 只缓存当天数据
      if (FormatDate(currentTime) === workDate)
        this.redisService.set(key, results, Math.floor(ttl));

      return results;
    } catch (error) {
      throw new BusinessException(error);
    }
  }
}
